/**
 * Copyright (C) 2025 pony working team
 * SPDX-License-Identifier: MulanPSL-2.0
 */

/**
 * YMODEM Protocol Introduction:
 *
 * YMODEM is a file transfer protocol commonly used for serial communication.
 * It is an extension of the XMODEM protocol, supporting batch file transfers,
 * larger packet sizes (128 or 1024 bytes), and file metadata such as filename and size.
 *
 * Protocol Format:
 * - The transfer begins with a special "filename packet" (block 0), which contains the filename and
 *   file size.
 * - Data packets follow, each containing a sequence number, data payload, and CRC/checksum.
 * - Each packet structure:
 *   +----------------+----------------+----------------+-------------------+-------------------+
 *   | Start Byte     | Packet Number  | Packet Number  | Data (128/1024 B) | CRC/Checksum (2B) |
 *   | (SOH/STX)      | (1 byte)       | Complement     |                   |                   |
 *   |                |                | (1 byte)       |                   |                   |
 *   +----------------+----------------+----------------+-------------------+-------------------+
 * - Control bytes:
 *   SOH (0x01): Start of 128-byte packet
 *   STX (0x02): Start of 1024-byte packet
 *   EOT (0x04): End of transmission
 *   ACK (0x06): Acknowledge
 *   NAK (0x15): Negative acknowledge
 *   CA  (0x18): Cancel
 *   'C' (0x43): Request for CRC mode
 *
 * - The receiver initiates the transfer by sending 'C' to request CRC mode.
 * - The sender transmits the filename packet, followed by data packets.
 * - The transfer ends with EOT, and the receiver acknowledges completion.
 */

/* includes (local) ----------------------------------------------------------*/
#include <ymodem.h>
/* includes (standard library, system) ---------------------------------------*/
#include <stdint.h>
#include <string.h>
/* includes (other library) --------------------------------------------------*/
/* includes (project) --------------------------------------------------------*/
#include <hal_part.h>

/* defines -------------------------------------------------------------------*/

#define YMODEM_PACKET_SEQNO_INDEX      (1)
#define YMODEM_PACKET_SEQNO_COMP_INDEX (2)

#define YMODEM_PACKET_HEADER   (3)
#define YMODEM_PACKET_TRAILER  (2)
#define YMODEM_PACKET_OVERHEAD (YMODEM_PACKET_HEADER + YMODEM_PACKET_TRAILER)
#define YMODEM_PACKET_SIZE     (128)
#define YMODEM_PACKET_1K_SIZE  (1024)

#define YMODEM_FILE_NAME_LENGTH (256)
#define YMODEM_FILE_SIZE_LENGTH (16)

#define YMODEM_SOH    (0x01) /* start of 128-byte data packet */
#define YMODEM_STX    (0x02) /* start of 1024-byte data packet */
#define YMODEM_EOT    (0x04) /* end of transmission */
#define YMODEM_ACK    (0x06) /* acknowledge */
#define YMODEM_NAK    (0x15) /* negative acknowledge */
#define YMODEM_CA     (0x18) /* two of these in succession aborts transfer */
#define YMODEM_CRC16  (0x43) /* 'C' == 0x43, request 16-bit CRC */
#define YMODEM_ABORT1 (0x41) /* 'A' == 0x41, abort by user */
#define YMODEM_ABORT2 (0x61) /* 'a' == 0x61, abort by user */

#define YMODEM_NAK_TIMEOUT (200)
#define YMODEM_MAX_ERRORS  (5)

/* typedefs ------------------------------------------------------------------*/
/* macro ---------------------------------------------------------------------*/

#define IS_AF(c)            ((c >= 'A') && (c <= 'F'))
#define IS_af(c)            ((c >= 'a') && (c <= 'f'))
#define IS_09(c)            ((c >= '0') && (c <= '9'))
#define ISVALIDHEX(c)       IS_AF(c) || IS_af(c) || IS_09(c)
#define ISVALIDDEC(c)       IS_09(c)
#define CONVERTDEC(c)       (c - '0')
#define CONVERTHEX_alpha(c) (IS_AF(c) ? (c - 'A' + 10) : (c - 'a' + 10))
#define CONVERTHEX(c)       (IS_09(c) ? (c - '0') : CONVERTHEX_alpha(c))

#define ymodem_send_byte(c) modem->putchar(c)
#define ymodem_abort()                                                                             \
    do {                                                                                           \
        ymodem_send_byte(YMODEM_CA);                                                               \
        ymodem_send_byte(YMODEM_CA);                                                               \
    } while (0)

/* variables (extern) --------------------------------------------------------*/
/* variables (local) ---------------------------------------------------------*/

static uint8_t __packet[YMODEM_PACKET_OVERHEAD + YMODEM_PACKET_1K_SIZE] = {0};
static uint8_t __filename[YMODEM_FILE_NAME_LENGTH] = {0};
static size_t __filesize;

/* variables (global) --------------------------------------------------------*/
/* inlines -------------------------------------------------------------------*/
/* functions -----------------------------------------------------------------*/

static uint32_t __str2int(uint8_t *inputstr, int32_t *intnum)
{
    uint32_t i = 0, res = 0;
    uint32_t val = 0;

    if (inputstr[0] == '0' && (inputstr[1] == 'x' || inputstr[1] == 'X')) {
        if (inputstr[2] == '\0') {
            return 0;
        }
        for (i = 2; i < 11; i++) {
            if (inputstr[i] == '\0') {
                *intnum = val;
                /* return 1; */
                res = 1;
                break;
            }
            if (ISVALIDHEX(inputstr[i])) {
                val = (val << 4) + CONVERTHEX(inputstr[i]);
            } else {
                /* return 0, Invalid input */
                res = 0;
                break;
            }
        }
        /* over 8 digit hex --invalid */
        if (i >= 11) {
            res = 0;
        }
    } else {
        /* max 10-digit decimal input */
        for (i = 0; i < 11; i++) {
            if (inputstr[i] == '\0') {
                *intnum = val;
                /* return 1 */
                res = 1;
                break;
            } else if ((inputstr[i] == 'k' || inputstr[i] == 'K') && (i > 0)) {
                val = val << 10;
                *intnum = val;
                res = 1;
                break;
            } else if ((inputstr[i] == 'm' || inputstr[i] == 'M') && (i > 0)) {
                val = val << 20;
                *intnum = val;
                res = 1;
                break;
            } else if (ISVALIDDEC(inputstr[i])) {
                val = val * 10 + CONVERTDEC(inputstr[i]);
            } else {
                /* return 0, Invalid input */
                res = 0;
                break;
            }
        }
        /* Over 10 digit decimal --invalid */
        if (i >= 11) {
            res = 0;
        }
    }

    return res;
}

/**
 * YMODEM First Packet (Block 0) File Metadata Format:
 *
 * The first packet sent in a YMODEM transfer (block 0) contains file metadata.
 * Its data payload is structured as follows:
 *
 *   <filename><NUL><filesize><space><modification date><space><mode><space><serial number><NUL>
 *
 * - <filename>:      The name of the file to be transferred (ASCII string).
 * - <NUL>:           A null byte (0x00) separating fields.
 * - <filesize>:      The size of the file in bytes (ASCII decimal string).
 * - <space>:         A space character (0x20) separating fields.
 * - <modification date>: (optional) File modification time as an octal string (UNIX time).
 * - <space>:         A space character (0x20).
 * - <mode>:          (optional) File mode (permissions) as an octal string.
 * - <space>:         A space character (0x20).
 * - <serial number>: (optional) File serial number (ASCII string).
 * - <NUL>:           A null byte (0x00) marking the end of metadata.
 *
 * Only <filename> and <filesize> are required; other fields are optional and may be omitted.
 * The remainder of the packet is padded with zeros.
 */
static int __parse_file(uint8_t *data)
{
    uint8_t *p = data + YMODEM_PACKET_HEADER;
    uint8_t file_size[YMODEM_FILE_SIZE_LENGTH];
    int32_t i;

    /* skip filename */
    for (i = 0; *p != 0; ++p) {
        if (i < YMODEM_FILE_NAME_LENGTH) {
            __filename[i++] = *p;
        }
    }
    __filename[i] = '\0';
    p++;
    for (i = 0; *p != ' '; ++p) {
        if (i < YMODEM_FILE_SIZE_LENGTH) {
            file_size[i++] = *p;
        }
    }
    file_size[i] = '\0';
    __filesize = 0;
    __str2int(file_size, (int32_t *)&__filesize);

    return 0;
}

static int __receive_byte(ymodem_t *modem, uint8_t *c, size_t timeout)
{
    int ch = modem->getchar(timeout);
    if (ch == -1) {
        return -1;
    }
    *c = (uint8_t)ch;
    return 0;
}

static int __receive_packet(ymodem_t *modem, uint8_t *data, int *length, size_t timeout)
{
    uint16_t i, packet_size;
    uint8_t c;
    *length = 0;
    if (__receive_byte(modem, &c, timeout) != 0) {
        return -1;
    }
    switch (c) {
    case YMODEM_SOH:
        packet_size = YMODEM_PACKET_SIZE;
        break;
    case YMODEM_STX:
        packet_size = YMODEM_PACKET_1K_SIZE;
        break;
    case YMODEM_EOT:
        return 0;
    case YMODEM_CA:
        if ((__receive_byte(modem, &c, timeout) == 0) && (c == YMODEM_CA)) {
            *length = -1;
            return 0;
        } else {
            return -1;
        }
    case YMODEM_ABORT1:
    case YMODEM_ABORT2:
        return 1;
    default:
        return -1;
    }
    *data = c;
    for (i = 1; i < (packet_size + YMODEM_PACKET_OVERHEAD); i++) {
        if (__receive_byte(modem, data + i, timeout) != 0) {
            return -1;
        }
    }
    if (data[YMODEM_PACKET_SEQNO_INDEX] != ((data[YMODEM_PACKET_SEQNO_COMP_INDEX] ^ 0xff) & 0xff)) {
        return -1;
    }
    *length = packet_size;
    return 0;
}

int ymodem_receive(ymodem_t *modem)
{
    if (modem == NULL) {
        return YMODEM_ERROR_INVAL;
    }
    size_t part_size = 0;
    if (hal_part_get_size(modem->part_id, &part_size) != 0) {
        return YMODEM_ERROR_PART_GET_SIZE;
    }
    size_t part_offset = 0;
    int packet_length, session_done, file_done, packets_received, errors, session_begin;

    for (session_done = 0, errors = 0, session_begin = 0;;) {
        for (packets_received = 0, file_done = 0;;) {
            switch (__receive_packet(modem, __packet, &packet_length, YMODEM_NAK_TIMEOUT)) {
            case 0:
                errors = 0;
                switch (packet_length) {
                case -1: /* Abort by sender */
                    ymodem_send_byte(YMODEM_ACK);
                    return YMODEM_ERROR_ABORTED_BY_SENDER;
                case 0: /* End of transmission */
                    ymodem_send_byte(YMODEM_ACK);
                    file_done = 1;
                    break;
                default: /* Normal packet */
                    if ((__packet[YMODEM_PACKET_SEQNO_INDEX] & 0xff) != (packets_received & 0xff)) {
                        ymodem_send_byte(YMODEM_NAK);
                    } else {
                        if (packets_received == 0) {
                            /* Filename packet */
                            if (__packet[YMODEM_PACKET_HEADER] != 0) {
                                /* Filename packet has valid data */
                                __parse_file(__packet);
                                /* Image size is greater than Flash size */
                                if (__filesize > part_size) {
                                    ymodem_abort();
                                    return YMODEM_ERROR_PART_NO_ENOUGH_SPACE;
                                }
                                if (hal_part_erase(modem->part_id, 0, part_size) != 0) {
                                    /* End session */
                                    ymodem_abort();
                                    return YMODEM_ERROR_PART_ERASE;
                                }
                                ymodem_send_byte(YMODEM_ACK);
                                ymodem_send_byte(YMODEM_CRC16);
                            } else {
                                /* Filename packet is empty, end session */
                                ymodem_send_byte(YMODEM_ACK);
                                file_done = 1;
                                session_done = 1;
                                break;
                            }
                        } else {
                            /* Data packet */
                            if (hal_part_write(modem->part_id, part_offset, packet_length,
                                               __packet + YMODEM_PACKET_HEADER) != 0) {
                                /* End session */
                                ymodem_abort();
                                return YMODEM_ERROR_PART_WRITE;
                            }
                            ymodem_send_byte(YMODEM_ACK);
                            part_offset += packet_length;
                        }
                        packets_received++;
                        session_begin = 1;
                    }
                }
                break;
            case 1:
                ymodem_abort();
                return YMODEM_ERROR_ABORTED_BY_USER;
            default:
                if (session_begin > 0) {
                    errors++;
                }
                if (errors > YMODEM_MAX_ERRORS) {
                    ymodem_abort();
                    return YMODEM_ERROR_TOO_MANY_ERRORS;
                }
                ymodem_send_byte(YMODEM_CRC16);
                break;
            }
            if (file_done != 0) {
                break;
            }
        }
        if (session_done != 0) {
            break;
        }
    }
    return YMODEM_SUCCESS;
}

size_t ymodem_get_filesize(void)
{
    return __filesize;
}

const char *ymodem_get_filename(void)
{
    return (char *)__filename;
}
